package xyz.lwm.lazycat.jdkhttp;

import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.http.BridgeInterceptor;
import okio.BufferedSource;
import okio.GzipSource;
import okio.Okio;
import xyz.lwm.lazycat.utility.CollectionUtil;
import xyz.lwm.lazycat.utility.It;
import xyz.lwm.lazycat.utility.PathUtil;
import xyz.lwm.lazycat.utility.StringUtil;
import xyz.lwm.lazycat.web.handle.Context;
import xyz.lwm.lazycat.web.proxy.ProxyClient;
import xyz.lwm.lazycat.web.settings.ProxyHttpSetting;

import java.io.IOException;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;

import static okhttp3.CookieJar.NO_COOKIES;
import static xyz.lwm.lazycat.jdkhttp.JdkHttpRequest.CHARSET_PATTERN;

/**
 * jdk http 代理客户端
 *
 * @author lwm
 */
public class JdkProxyClient implements ProxyClient {

    private final OkHttpClient httpClient;

    public JdkProxyClient() {
        httpClient = new OkHttpClient.Builder()
                .connectTimeout(Duration.ofSeconds(120))
                .readTimeout(Duration.ofSeconds(60))
                .writeTimeout(Duration.ofSeconds(60))
                .pingInterval(Duration.ofSeconds(30))
                .addInterceptor(new BridgeInterceptor(NO_COOKIES))
                .build();
    }

    @Override
    public void proxyHttp(Context ctx, ProxyHttpSetting proxy) {
        // target host
        String targetHost = It.of(proxy.getTargetHost()).getOr("localhost");

        // path
        String newPath = It.of(ctx.getPath()).let(path -> {
            // rewrite 为空时, 则使用 原 path
            if (StringUtil.isBlank(proxy.getRewritePath())) {
                return path;
            }
            // 否则截取替换, 例: prefix path: /api/v1/ -> rewrite path: / ,
            // 可以完成移除前缀 path: /api/v1/users/1 -> /users/1
            String normalizePath = PathUtil.normalizePath(path);
            String prefixPath = PathUtil.normalizePath(proxy.getPrefixPath());
            String subPath = normalizePath.substring(prefixPath.length());
            return PathUtil.of(proxy.getRewritePath()).append(subPath).toString();
        }).get();

        // url (暂时只支持 http)
        String url = "http://" + PathUtil.of(targetHost + ":" + proxy.getListenPort()).append(newPath);

        // request http method
        String method = ctx.req().getMethod();

        // request headers
        Map<String, List<String>> headers = ctx.req().getHeaders();
        Headers.Builder headsBuilder = new Headers.Builder();
        headers.forEach((k, v) -> {
            if (CollectionUtil.isNotEmpty(v)) {
                v.forEach(val -> headsBuilder.add(k, val));
            }
        });

        // request body
        RequestBody body = It.of(ctx.req().getBodyBytes())
                .filter(bytes -> bytes.length > 0)
                .let(RequestBody::create).getOr(null);

        Request.Builder builder = new Request.Builder().url(url)
                .headers(headsBuilder.build()).method(method, body);
        try {
            Response response = httpClient.newCall(builder.build()).execute();
            Response uncompressed = uncompress(response);

            ctx.res().setStatus(uncompressed.code());
            Headers respheaders = uncompressed.headers();
            respheaders.forEach(e -> {
                ctx.res().addHeader(e.getFirst(), e.getSecond());
            });
            if (uncompressed.body() != null) {
                ctx.res().write(uncompressed.body().string());
            }
        } catch (IOException e) {
            ctx.res().setStatus(502);
            ctx.res().write("BAD FORWARD");
        }

    }

    /**
     * 自动解压缩
     *
     * @param response 响应
     * @return new response or self
     * @throws IOException if error
     */
    private Response uncompress(Response response) throws IOException {
        if (response.body() == null) {
            return response;
        }
        String contentEncoding = response.header("Content-Encoding");
        if ("gzip".equalsIgnoreCase(contentEncoding) ||
                "deflate".equalsIgnoreCase(contentEncoding)) {
            BufferedSource buffer = Okio.buffer(new GzipSource(response.body().source()));
            Charset charset = getCharset(response);
            String body = buffer.readByteString().string(charset);
            // 移除 Content-Encoding, 防止下游继续解压
            return response.newBuilder().removeHeader("Content-Encoding")
                    .body(ResponseBody.create(body, response.body().contentType())).build();
        }
        return response;
    }

    private Charset getCharset(Response response) {
        return It.of(response.header("Content-Type"))
                .filter(StringUtil::isNotBlank)
                .let(CHARSET_PATTERN::matcher).filter(Matcher::find)
                .let(matcher -> matcher.group(1))
                .let(this::parseCharset)
                .getOr(Charset.defaultCharset());
    }

    private Charset parseCharset(String charset) {
        try {
            return Charset.forName(charset);
        } catch (Exception e) {
            return null;
        }
    }

}
